Skip to content

Sports Results Completed#87

Open
DSills735 wants to merge 2 commits into
the-csharp-academy:mainfrom
DSills735:main
Open

Sports Results Completed#87
DSills735 wants to merge 2 commits into
the-csharp-academy:mainfrom
DSills735:main

Conversation

@DSills735

Copy link
Copy Markdown

No description provided.

@chrisjamiecarter chrisjamiecarter left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @DSills735 👋,

Excellent work on your Sports Results Notifier project submission 🎉!

I have performed a peer review. Review/ignore any comments as you wish.

Your code is clean, well-organised, and follows modern C# conventions like file-scoped namespaces. You've done a great job with defensive programming — null checks on HTML nodes, TryParse for score validation, and using environment variables for credentials are all solid practices. The project builds successfully with zero warnings, which is a great starting point.

Project-Wide Patterns:

  • Static method coupling — All public methods are static, creating tight coupling between classes. Introducing interfaces and instance-based services would improve testability.
  • Synchronous I/O — Web scraping and email sending are synchronous. Converting to async/await would make the application more responsive.
  • No error handling for network calls — If the basketball website is unreachable, the app will crash. A try-catch around the web request would make it more resilient.

I will go ahead and mark as approved, keep up the excellent work on the next projects! 😊

Thanks,
@chrisjamiecarter 👍

{
public static void Main(string[] args)
{
TimeOnly runningTime = new TimeOnly(23, 0, 0); // 11 PM

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Magic Number

💡 The scheduled run time (23:00) is hardcoded as a magic number. Consider extracting it to a named constant or configuration for better clarity and maintainability:

private const int ScheduledHour = 23;
private static readonly TimeOnly RunningTime = new(ScheduledHour, 0, 0);

Comment on lines +12 to +13
TimeOnly now = TimeOnly.FromDateTime(DateTime.Now);
DateOnly today = DateOnly.FromDateTime(DateTime.Now);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Redundant DateTime.Now Calls

💡 DateTime.Now is called twice within the same loop iteration. While the time difference is negligible here, it's cleaner and more consistent to capture it once and reuse:

DateTime now = DateTime.Now;
TimeOnly currentTime = TimeOnly.FromDateTime(now);
DateOnly today = DateOnly.FromDateTime(now);

lastRun = today;
}

Thread.Sleep(10000);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 Blocking Sleep on Main Thread

💡 Thread.Sleep(10000) blocks the current thread and prevents clean application shutdown. Consider using await Task.Delay(10000) and making Main async:

public static async Task Main(string[] args)
{
    // ...
    await Task.Delay(10000);
}

This also improves responsiveness and enables graceful cancellation with CancellationToken.

TimeOnly runningTime = new TimeOnly(23, 0, 0); // 11 PM
DateOnly lastRun = DateOnly.MinValue;

while (true)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 No Graceful Shutdown Handling

💡 The application runs an infinite while(true) loop with no means to exit cleanly. Consider adding a CancellationToken or handling Console.CancelKeyPress to allow a graceful stop when the user presses Ctrl+C:

CancellationTokenSource cts = new();
Console.CancelKeyPress += (_, e) => { e.Cancel = true; cts.Cancel(); };
// Use cts.Token in your loop and Task.Delay

Comment on lines +22 to +23
WinnerScore = winner == team1 ? team1Score : team2Score;
LoserScore = loser == team1 ? team1Score : team2Score;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Redundant Constructor Logic

💡 WinnerScore and LoserScore are computed from other properties passed to the constructor. They could be refactored as computed (expression-bodied) properties, eliminating the need to store them and simplifying the constructor:

public int WinnerScore => Team1Score > Team2Score ? Team1Score : Team2Score;
public int LoserScore => Team1Score < Team2Score ? Team1Score : Team2Score;

This removes the ternary logic and Winner/Loser string comparisons from the constructor.

public static void Scan()
{
HtmlWeb web = new HtmlWeb();
HtmlDocument document = web.Load("https://www.basketball-reference.com/boxscores/");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Synchronous Web Scraping

💡 HtmlWeb.Load() performs synchronous I/O, blocking the thread. Consider using the async overload await web.LoadAsync(...) for better resource utilization, and make Scan() an async method.

gameList.Add(new Game(team1, team1Score, team2, team2Score, winner, loser));
}

Email.Send(gameList);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 Tight Coupling via Static Call

💡 Email.Send(gameList) is called directly as a static method, creating tight coupling between BasketballScanner and Email. Consider making these instance methods and using dependency injection or passing an abstraction (e.g., IEmailService) to improve testability and flexibility.

var team2Node = game.SelectSingleNode(".//tr[2]/td[1]/a");
var team2ScoreNode = game.SelectSingleNode(".//tr[2]/td[2]");

if (team1Node == null || team1ScoreNode == null || team2Node == null || team2ScoreNode == null)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Null Checks on HTML Nodes

Comment on lines +36 to +37
if (!int.TryParse(team1ScoreNode.InnerText.Trim(), out int team1Score) ||
!int.TryParse(team2ScoreNode.InnerText.Trim(), out int team2Score))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Input Validation with TryParse

Comment on lines +12 to +17
string fromEmail = Environment.GetEnvironmentVariable("EMAIL_FROM")
?? throw new InvalidOperationException("EMAIL_FROM environment variable is not set.");
string toEmail = Environment.GetEnvironmentVariable("EMAIL_TO")
?? throw new InvalidOperationException("EMAIL_TO environment variable is not set.");
string password = Environment.GetEnvironmentVariable("EMAIL_PASSWORD")
?? throw new InvalidOperationException("EMAIL_PASSWORD environment variable is not set.");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Secure Credential Handling

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants